ES6 的迭代器和生成器
大量参考 ES6系列---迭代器(Iterator)与生成器(Generator) 参考资料 ES6 迭代器
循环语句的问题
var colors = ["red", "green", "blue"];
for(var i=0; i<colors.length; i++){
console.log(colors[i]);
}
在 ES6 之前,这种标准的 for 循环,通过变量来跟踪数组的索引(变量 i)。如果多个循环嵌套就需要追踪多个变量,代码复杂度会大大增加,也容易产生错用循环变量的 bug。
什么是迭代器
用 ES5 语法模拟创建一个迭代器
function createIterator(items) {
var i = 0;
return { // 返回一个迭代器对象
next: function() { // 迭代器对象一定有个next()方法
var done = (i >= items.length);
var value = !done ? items[i++] : undefined;
return { // next()方法返回结果对象
value: value,
done: done
};
}
};
}
var iterator = createIterator([1, 2, 3]);
console.log(iterator.next()); // "{ value: 1, done: false}"
console.log(iterator.next()); // "{ value: 2, done: false}"
console.log(iterator.next()); // "{ value: 3, done: false}"
console.log(iterator.next()); // "{ value: undefiend, done: true}"
// 之后所有的调用都会返回相同内容
console.log(iterator.next()); // "{ value: undefiend, done: true}"
借助这个迭代器对象,来改造刚开始那个标准的 for 循环
var colors = ["red", "green", "blue"];
var iterator = createIterator(colors);
while(!iterator.next().done){
console.log(iterator.next().value);
}
什么是生成器
生成器是一种返回迭代器的函数,通过 function 关键字后的星号(*)来表示,函数中会用到新的关键字 yield。
function *createIterator(items) {
for(let i=0; i<items.length; i++) {
yield items[i];
}
}
let iterator = createIterator([1, 2, 3]);
// 既然生成器返回的是迭代器,自然就可以调用迭代器的next()方法
console.log(iterator.next()); // "{ value: 1, done: false}"
console.log(iterator.next()); // "{ value: 2, done: false}"
console.log(iterator.next()); // "{ value: 3, done: false}"
console.log(iterator.next()); // "{ value: undefiend, done: true}"
// 之后所有的调用都会返回相同内容
console.log(iterator.next()); // "{ value: undefiend, done: true}"
用 ES6 的生成器,大大简化了迭代器的创建过程。给生成器函数 createIterator()
传入一个 items 数组,函数内部,for 循环不断从数组中生成新的元素放入迭代器中,每遇到一个 yield 语句循环都会停止;每次调用迭代器的 next()
方法,循环便继续运行并停止在下一条 yield 语句处。
下面来看下生成器是怎么创建的
ES6 Generator 生成器
ES6 新引入了 Generator 函数,可以通过 yield
关键字,把函数的执行流挂起,为改变执行流程提供了可能,从而为异步编程提供解决方案。
这个就和 Unity 里使用的协程用法是一致的
Generator 有两个区分于普通函数的部分:
- 在
function
后面,函数名之前有个*
- 函数内部有
yield
表达式。
其中 *
用来表示函数为 Generator 函数,yield
用来定义函数内部的状态。
下面是它的创建方式
// 生成器是个函数
function* func(){
console.log("one");
yield '1';
console.log("two");
yield '2';
console.log("three");
return '3';
}
// 生成器内部也可以通过 for 循环不断从数组中生成新的元素放入迭代器中
function *createIterator(items) {
for(let i=0; i<items.length; i++) {
yield items[i];
}
}
// 可以用函数表达式方式书写:
let createIterator = function *(item) { ... }
// 也可以添加到对象中
let o = {
createIterator: function *(items) { ... }
};
let iterator = o.createIterator([1, 2, 3]);
// ES6风格的对象方法简写方式:
let o = {
*createIterator(items) { ... }
};
let iterator = o.createIterator([1, 2, 3]);
可迭代(Iterable)对象
在 ES6 中,所有的集合对象(数组、Set集合及Map集合)和字符串都是可迭代对象,可迭代对象都绑定了默认的迭代器。
var colors = ["red", "green", "blue"];
for(let color of colors){
console.log(color);
}
for-of 循环,可作用在可迭代对象上,正是利用了可迭代对象上的默认迭代器。大致过程是:for-of 循环每执行一次都会调用可迭代对象的 next()
方法,并将迭代器返回的结果对象的 value 属性存储在变量中,循环将继续执行这一过程直到返回对象的 done 属性的值为 true。
如果只需要迭代数组或集合中的值,可以用 for-of 循环代替 for循环
生成器执行机制
调用 Generator 函数和调用普通函数一样,在函数名后面加上 ()
即可,但是 Generator 函数不会像普通函数一样立即执行,而是返回一个指向内部状态对象的指针,所以要调用遍历器对象 Iterator
的 next
方法,指针就会从函数头部或者上一次停下来的地方开始执行。
注意:除了使用 next
,还可以使用 for... of
循环遍历 Generator 函数生产的 Iterator
对象。
function* func() {
console.log("one");
yield '1';
console.log("two");
yield '2';
console.log("three");
return '3';
}
let f = func();
/**
* 第一次调用 next 方法时,从 Generator 函数的头部开始执行,
* 先是打印了 one ,执行到 yield 就停下来,并将 yield 后边表
* 达式的值 '1',作为返回对象的 value 属性值,此时函数还没有
* 执行完, 返回对象的 done 属性值是 false。
*/
console.log(f.next());
// one
// {value: "1", done: false}
/* 第二次调用 next 方法时,同上步 */
console.log(f.next());
// two
// {value: "2", done: false}
/**
* 第三次调用 next 方法时,先是打印了 three ,然后执行了函数
* 的返回操作,并将 return 后面的表达式的值,作为返回对象的
* value 属性值,此时函数已经结束,多以 done 属性值为true 。
*/
console.log(f.next());
// three
// {value: "3", done: true}
/**
* 第四次调用 next 方法时, 此时函数已经执行完了,所以返回
* value 属性值是 undefined ,done 属性值是 true 。如果执
* 行第三步时,没有 return 语句的话,就直接返回 {value: undefined, done: true}。
*/
console.log(f.next());
// {value: undefined, done: true}
访问默认迭代器
可迭代对象,都有一个 Symbol.iterator 方法,for-of 循环时,通过调用 colors 数组的 Symbol.iterator 方法来获取默认迭代器的(这一过程是在 JavaScript 引擎背后完成的)
let values = [1, 2, 3];
let iterator = values[Symbol.iterator]();
console.log(iterator.next()); // "{ value: 1, done: false}"
console.log(iterator.next()); // "{ value: 2, done: false}"
console.log(iterator.next()); // "{ value: 3, done: false}"
console.log(iterator.next()); // "{ value: undefined, done: true}"
在这段代码中,通过 Symbol.iterator 获取了数组 values 的默认迭代器,并用它遍历数组中的元素。在 JavaScript 引擎中执行 for-of 循环语句也是类似的处理过程。
用 Symbol.iterator 属性来检测对象是否为可迭代对象:
function isIterator(object) {
return typeof object[Symbol.iterator] === "function";
}
console.log(isIterable([1, 2, 3])); // true
console.log(isIterable(new Set())); // true
console.log(isIterable(new Map())); // true
console.log(isIterable("Hello")); // true
创建可迭代对象
在创建对象时,给 Symbol.iterator 属性添加一个生成器,则可以将其变成可迭代对象:
let collection = {
items: [],
*[Symbol.iterator]() { // 将生成器赋值给对象的 Symbol.iterator 属性来创建默认的迭代器
for(let item of this.items) {
yield item;
}
}
};
collection.items.push(1);
collection.items.push(2);
collection.items.push(3);
for(let x of collection) {
console.log(x);
}
不同集合的默认迭代器
ES6中的集合对象,数组、Set集合和 Map集合,都内建了三种迭代器:
entries()
返回一个迭代器,其值为多个键值对。如果是数组,第一个元素是索引位置;如果是 Set 集合,第一个元素与第二个元素一样,都是值。values()
返回一个迭代器,其值为集合的值。keys()
返回一个迭代器,其值为集合中的所有键名。如果是数组,返回的是索引;如果是Set集合,返回的是值(Set的值被同时用作键和值)。
每个集合类型都有一个默认的迭代器,在 for-of 循环中,如果没有显式指定则使用默认的迭代器。按常规使用习惯,数组和 Set 集合的默认迭代器是 values()
,Map 集合的默认迭代器是 entries()
。
let colors = [ "red", "green", "blue"];
let tracking = new Set([1234, 5678, 9012]);
let data = new Map();
data.set("title", "Understanding ECMAScript 6");
data.set("format", "print");
// 与调用colors.values()方法相同
for(let value of colors) {
console.log(value);
}
// 与调用tracking.values()方法相同
for(let num of tracking) {
console.log(num);
}
// 与调用data.entries()方法相同
for(let entry of data) {
console.log(entry);
}
返回值为;
"red"
"green"
"blue"
1234
5678
9012
["title", "Understanding ECMAScript 6"]
["format", "print"]
for 配合解构
for-of 循环配合解构特性,操纵数据会更方便:
for(let [key, value] of data) {
console.log(key + "=" + value);
}
用展开运算符操纵
let set = new Set([1, 2, 3, 4, 5]),
array = [...set];
console.log(array); // [1,2,3,4,5]
展开运算符可以操作所有的可迭代对象,并根据默认迭代器来选取要引用的值,从迭代器读取所有值。然后按返回顺序将它们依次插入到数组中。因此如果想将可迭代对象转换为数组,用展开运算符是最简单的方法。
next 方法返回值
next()
方法:一般情况下,next
方法不传入参数的时候,yield
表达式的返回值是 undefined
。当 next
传入参数的时候,该参数会作为上一步 yield
的返回值。
function* sendParameter(){
console.log("start");
var x = yield '2';
console.log("one:" + x);
var y = yield '3';
console.log("two:" + y);
console.log("total:" + (x + y));
}
next
不传参
var sendp1 = sendParameter();
sendp1.next();
// start
// {value: "2", done: false}
sendp1.next();
// one:undefined
// {value: "3", done: false}
sendp1.next();
// two:undefined
// total:NaN
// {value: undefined, done: true}
next
传参
var sendp2 = sendParameter();
sendp2.next(10);
// start
// {value: "2", done: false}
sendp2.next(20);
// one:20
// {value: "3", done: false}
sendp2.next(30);
// two:30
// total:50
// {value: undefined, done: true}
结束函数 return 方法
注意:这个是 return
方法,不是 return
关键字
return
方法返回给定值,并结束遍历 Generator 函数。同上,该方法提供参数时,返回该参数;不提供参数时,返回 undefined
。
function* foo(){
yield 1;
yield 2;
yield 3;
}
var f = foo();
f.next();
// {value: 1, done: false}
f.return("foo");
// {value: "foo", done: true}
f.next();
// {value: undefined, done: true}
抛出异常 throw 方法
注意:这个是 throw
方法,不是 throw
关键字
throw
方法可以在 Generator 函数体外面抛出异常,在函数体内部捕获。
如下例子:遍历器对象抛出了两个错误,第一个被 Generator 函数内部捕获,第二个因为函数体内部的 catch
函数已经执行过了,不会再捕获这个错误,所以这个错误就抛出 Generator 函数体,被函数体外的 catch
捕获。
var g = function* () {
try {
yield;
} catch (e) {
console.log('catch inner', e);
}
};
var i = g();
i.next();
try {
i.throw('a');
i.throw('b');
} catch (e) {
console.log('catch outside', e);
}
// catch inner a
// catch outside b
yield* 表达式
yield*
表达式表示 yield
返回一个遍历器对象,用于在 Generator 函数内部,调用另一个 Generator 函数。
function* callee() {
console.log('callee: ' + (yield));
}
function* caller() {
while (true) {
yield* callee();
}
}
const callerObj = caller();
callerObj.next();
// {value: undefined, done: false}
callerObj.next("a");
// callee: a
// {value: undefined, done: false}
callerObj.next("b");
// callee: b
// {value: undefined, done: false}
等同于
function* caller() {
while (true) {
for (var value of callee) {
yield value;
}
}
}